The DirectX 8 Pixel Shader
Introduction.
The introduction of a programmable pipeline to DirectX allows far greater control over how objects are rendered to the screen. DirectX 8 introduced two new programmable sections, the Vertex Shader, and the Pixel Shader. The vertex shader can control vertex positions, colors, and texturing coordinates. The pixel shader provides control over the way each individual pixel is rendered to the screen. This tutorial will cover the basics on setting up the pixel shader, and writing simple pixel shader programs. There are a number of example source files provided with this tutorial, to help the understanding of the contents. (Please note that the example source does not support any error checking, and is in general, very messy. The relevant sections are highlighted in this document, the source was only provided to allow you to compile the examples, and modify the source to examine the pixel shader features.)
Please note, that if you do not wish to use the provided source, or integrate pixel shader functionality into your 3d engine, you can write pixel shader code directly into a program provided with the DirectX 8.1 SDK called "MFC Pixel Shader".
Setting Up The Pixel Shader.
(This section can be skipped if you are using the MFC Pixel Shader tool.)
The Direct3DX utility library provides many useful functions for setting up a pixel shader. Adding pixel shader support is a very simple task, and requires little effort. Each shader requires its own handle, and its own buffer for storing the compiled pixel shader source code. The shader must then be assembled, created in the D3D device, and enabled. This is illustrated in the following code segment.
DWORD m_hPixelShader;
LPD3DXBUFFER pCode;
void MakeShader(void) {
D3DXAssembleShaderFromFile("invert.txt",0,NULL,&pCode,NULL);
g_pd3dDevice->CreatePixelShader( (DWORD*)pCode->GetBufferPointer(),
&m_hPixelShader );
g_pd3dDevice->SetPixelShader( m_hPixelShader );
}
The D3DXAssembleShaderFromFile takes 5 parameters:
- The pixel shader source code filename (eg: "invert.txt")
- A flag to indicate whether debug comments should be inserted by the assembler, and whether the assembler should validate the constraints in the source file. It is recommended you set this parameter to 0.
- A pointer to a buffer, which will contain the compiled binary code of the pixel shader. (eg: &pCode)
- A pointer to a buffer, which will contain the errors which occurred during compilation. (eg: &pError) Note that the third parameter is ignored, and hence should be set to NULL.
So, in this example, the pixel shader source file "invert.txt" is compiled and the binary code is placed into the pCode buffer. (Error support, and detecting the supported pixel shader version is demonstrated in the final example.)
Once the pixel shader has been assembled, the pixel shader must be created on the D3D Device. The CreatePixelShader function takes two parameters:
- A pointer to a buffer, which contains the binary instructions for the pixel shader.
- A handle to the pixel shader.
To enable the pixel shader, the SetPixelShader function is called, with the pixel shader handle as its parameter. If you wish to use multiple different pixel shaders, you need to call this function every time you wish to switch to a different pixel shader. Since the CreatePixelShader function is not part of the Direct3DX utility library, it will not accept the D3DX buffer, and so the buffer needs to be casted to a DWORD pointer. (GetBufferPointer() returns a void pointer)
Deleting the Pixel Shader.
(This section can be skipped if you are using the MFC Pixel Shader tool.)
This is a very simple process. First you must un-set the pixel shader, which is done by calling the SetPixelShader function and provided it with a NULL parameter. The pixel shader can then be deleted by calling the DeletePixelShader function and providing it with the handle to the pixel shader you wish to delete.
void DeleteShader(void) {
g_pd3dDevice->SetPixelShader(NULL);
g_pd3dDevice->DeletePixelShader(m_hPixelShader);
}
Introducing the Pixel Shader language.
In general, a pixel shader source code file is split into 4 sections.
- The pixel shader version identifier (eg: ps.1.0)
- The constant declaration section (eg: def c0, 1.0f, 0.0f, 0.0f, 1.0f)
- The texture reading section (eg: tex t0)
- The arithmetic section (eg: mov r0,t0)
The pixel shader language, is similar to assembly language, however it is far simpler to understand. There are a number of registers provided with each version of the pixel shader language. (A register is like a variable, with a specific name and data type). The registers are:
Cn - Constant registers, these are like your #define's in c. They do not change throughout the pixel shaders execution.
Rn - Temporary registers, these are like normal variables, you can add, subtract, and multiply their contents.
Tn - Texture registers, these contain texture data. (eg: A specific pixel from your texture, or texture coordinates in ps.1.4)
Vn - Color registers, these contain specific color data, such as diffuse lighting information for each pixel on your screen.
Register r0 - is a special temporary register, as it also corresponds to the output value of the pixel. (ie: r0 contains the data which is displayed on the screen)
The pixel shader language also has a large number of instructions. Some simple and useful instructions are listed bellow.
ps
This sets the pixel shader version. This instruction must be the first line of every pixel shader source file. Valid values are ps.1.0, ps.1.1, ps.1.2, ps.1.3, ps.1.4
def
This allows you to define a constant to be used within the pixel shader. It takes 5 parameters, the first being the constant you wish to define, and the next four represent its floating point red, green, blue and alpha values.
eg:
def c0, 1.0f, 1.0f, 1.0f, 1.0f
This defines the constant register c0, as pure white.
add
This allows you to add registers together, and place the result into a destination register. The addition is performed separately on each red, green, blue and alpha value.
eg:
add r0, t0, r1
This will add t0's red value, to r1's red value, and place the result into r0's red value.
Likewise for the green, blue, and alpha values. (ie: r0=t0+r1)
sub
Much like add, except you subtract the values.
eg:
sub r0, t0, r1
This will subtract r1 from t0, for each color value, and place the result in r0. (ie: r0=t0-r1)
mov
This moves, or copies one registers contents to another.
eg:
mov r0,r1
This will copy each color value of r1 into r0. (ie: r0=r1)
mul
This multiplies the color values of two registers and places the result into a destination register.
eg:
mul r0, t0, r1
This will multiply the blue value of t0, with the blue value of r1, and place the result into the blue value of r0. Similarly for red, green, and alpha. (ie: r0=t0*r1)
tex
This loads texture data from a texture into a texture register for later manipulation. (Note that for pixel shader 1.4, you must use the texld instruction instead)
eg:
tex t0
This will load texture data from texture 0 into the t0 register. (ie: t0=Texture0[x,y])
A full list of instructions can be found in the DirectX 8.1 SDK. An online version of the DirectX 8.0 SDK documentation of pixel shader instructions is available from msdn.microsoft.com.
Writing the first pixel shader program.
To begin with, a simple program that fills the screen with red, is illustrated bellow:
ps.1.0
def c0, 1.0f, 0.0f, 0.0f, 1.0f
mov r0,c0
The first line defines the pixel shader language version as 1.0, the second line defines a constant with full red value, and full alpha value. Note the order is red, green, blue, then alpha. The last instruction moves the contents of register c0, to register r0. That is, it outputs the color red to the screen.
Writing more complicated shader programs.
Naturally, you would want to do something a little more complicated than drawing pure colors to the screen. A simple example of the added capabilities that the pixel shader provides is an invert function. Previously it would have been impossible to invert the image rendered to the screen in hardware. However with a few lines of pixel shader code, you can easily invert the entire output of the images rendered by your 3d engine.
ps.1.0
def c0, 1.0f, 1.0f,1.0f, 1.0f
tex t0
sub r0,c0,t0
Here, we define the register c0 as pure white, then, we load the texture data into the register t0, and then subtract the texture data from pure white and place the result into the output register, r0. This would be similar to the following pseudo C code:
t0 = Texture0[x,y];
r0 = 1-t0;
With these instructions one can easily perform functions such as brightening or darkening images in less than five lines of code. (Note that the invert can be done in fewer lines than shown above by using modifiers)
Setting up the Pixel Shader for using Multiple Textures.
(This section can be skipped if you are using the MFC Pixel Shader tool.)
There is a limit to how much can be done with one texture and the pixel shader. The pixel shaders power really comes to show when two or more textures are used. In order to utilize multiple textures, you must add extra texture coordinates to your model. A brief outline of the changes are shown below:
struct DefaultModelVertex
{
D3DXVECTOR3 position;
D3DXVECTOR3 normal;
FLOAT tu1, tv1;
FLOAT tu2, tv2;
};
#define D3DFVF_DefaultModelVertex (D3DFVF_XYZ|D3DFVF_NORMAL|D3DFVF_TEX2)
...
g_pd3dDevice->SetTexture( 0, pTexture0 );
g_pd3dDevice->SetTexture( 1, pTexture1 );
The model vertex's must now include extra texture coordinate information (tu2,tv2), and the flexible vertex format flag must now be set to D3DFVF_TEX2. Note that you also need to load the two textures, and set them as active when you wish to render your model.
Pixel Shaders with Multiple Textures.
Using multiple textures in a pixel shader program allows you to perform various filtering functions, as well as blending operations. A simple example of adding two textures together is illustrated below.
ps.1.0
tex t0
tex t1
mov r0,t1
add r0,r0,t0
This loads pTexture0's texture data into the register t0, and pTexture1's data into register t1. The mov instruction then copies the data from register t1 to register r0. The add instruction then adds the value of t0 to r0 and places the result into r0. Expressed as pseudo C code this would be:
t0=Texture0[x,y];
t1=Texture1[i,j];
r0=t1;
r0+=t0;
By experimenting with the various instructions provided to you by the pixel shader, you can create some very interesting effects. By manipulating texture coordinates one can create various effects, such as blurring, and edge filtering, however, Pixel Shader 1.4 provides you with much simpler instructions to obtain this power.
Pixel Shader Version 1.4
At the time of this writing, the ATI Radeon cards were the only cards that supported Pixel Shader Version 1.4. Pixel Shader 1.4 provides you with far more powerful instructions than previous pixel shader versions.
The most important added instructions are:
phase
This moves you from the first phase of execution to the second. This is somewhat like being able to be able to do twice as much texture loading and manipulation than you could in previous versions of the pixel shader language.
texcrd
This moves the texture coordinate values of a texture into the destination register.
eg:
texcrd r1,t0
This will place the x,y texture coordinate positions of the texture in t0 to the register r1.
texld
This will load the texture at the coordinates specified in the source register to the destination register.
eg:
texld r0,r1
A small example program that demonstrates the power unleashed by pixel shader 1.4 is listed below:
ps.1.4
texld r1,t0
phase
texld r0,r1
Now we define the pixel shader version as 1.4, and perform a texture loading operation, which places the texture data (ie the RGBA values) into register r1.
The phase instruction moves us into the next phase, allowing us to perform texture loading operations again. We now load the texture data pointed to by r1 into r0. This effect is better illustrated with some pseudo C code:
r1=Texture[x,y];
r0=Texture[r1];
Needless to say, an amazing amount of effects can be performed using Pixel Shader 1.4.
Pixel Shader Modifiers
Pixel shader modifiers, allow you to mask certain parts of source and destination registers, perform bitwise shifts on data, and alter the way certain instructions operate. An example of converting a colored image to a black and white image is shown below.
ps.1.4
def c0,0.3f,0.3f,0.3f,0.3f
texld r1,t1
mov r3,r1.r
mul r0,r3,c0
mov r3,r1.g
mul r3,r3,c0
add r0,r0,r3
mov r3,r1.b
mul r3,r3,c0
add r0,r0,r3
The pseudo-C code for this listing is:
r1=Texture1[x,y]
r3=red(r1)
r0=r3*0.3
r3=green(r1)
r0+=r3*0.3
r3=blue(r1)
r0+=r3*0.3
There are a large number of modifiers available. In this example we used the .r,.g,.b modifiers which copy the red, green and blue data to all values of the register. So mov r3,r1.r will copy the red value of r1 into the red, green, blue and alpha values of r3. Modifiers can also be used on instructions to alter their behavior. A full list of the modifiers is available in the DirectX 8.1 SDK.
Conclusion.
This tutorial was meant to provide a small demonstration and a simple introduction to the pixel shader language, and provide the basics steps for including pixel shader support to an existing 3d engine. The include source provides extra examples of more effects that one can do with the pixel shader.
If you have any questions, or have created any special effects you wish to share, feel free to contact me at: error64@tartarus.uwa.edu.au
NOTE: I do NOT guarantee that the information in this tutorial is 100% correct. I may provided an updated version of this document at a later date. Also, you will require MS VC++ and DirectX 8.1 to compile the source, furthermore you will need to provide three texture files, texture.bmp, texture0.bmp and texture1.bmp. (These need to be square textures, preferably all different from one another.)